package org.internna.expensetracker.model.security;

import java.util.Set;
import java.util.Locale;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.SortedSet;
import java.util.Collections;
import javax.persistence.Column;
import javax.persistence.OneToOne;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.CascadeType;
import javax.validation.constraints.Size;
import javax.validation.constraints.NotNull;
import org.apache.commons.collections.Predicate;
import org.internna.expensetracker.model.Payee;
import org.internna.expensetracker.model.Account;
import org.internna.expensetracker.model.Dashboard;
import org.internna.expensetracker.util.SecurityUtils;
import org.internna.expensetracker.model.budget.Budget;
import org.internna.expensetracker.ExpenseTrackerException;
import org.internna.expensetracker.model.FinancialInstitution;
import org.springframework.util.CollectionUtils;
import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.serializable.RooSerializable;
import static org.springframework.util.StringUtils.hasText;
import static org.internna.expensetracker.util.StringUtils.SHA1;
import org.internna.expensetracker.model.Subcategory;
import javax.validation.constraints.Pattern;

@RooJavaBean
@RooSerializable
@RooEntity(table = "USERS")
public class UserDetails {

    private static final long serialVersionUID = 1929827454907681225L;

    @NotNull
    private String password;

    @NotNull
    @Size(min = 4)
    @Column(unique = true)
    private String username;

    private transient String salt;

    @NotNull
    private Boolean enabled;

    @NotNull
    @Size(min = 4)
    private String name;

    @NotNull
    @Pattern(regexp = "^([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})$")
    private String email;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "AUTHORITIES", joinColumns = { @JoinColumn(name = "USERNAME", referencedColumnName = "username", nullable = false) }, inverseJoinColumns = { @JoinColumn(name = "AUTHORITY", referencedColumnName = "authority", nullable = false) })
    private Set<Authority> roles = new HashSet<Authority>();

    @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "owner")
    private Set<Account> accounts = new TreeSet<Account>();

    @NotNull
    @OneToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
    private Dashboard dashboard;

    @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "owner")
    private Set<Payee> payees = new HashSet<Payee>();

    @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "owner")
    private Set<FinancialInstitution> institutions = new HashSet<FinancialInstitution>();

    @OneToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "owner")
    private Budget budget;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "owner")
    private Set<Subcategory> subcategories = new HashSet<Subcategory>();

    /**
     * Encodes user password using SHA-1. Invoke setSalt first if you want a salted hashed result.
     *
     **/
    public void setPassword(String password) {
        if (hasText(password)) {
            try {
                this.password = SHA1(salt, password);
            } catch (Exception e) {
                throw new ExpenseTrackerException(e);
            }
        }
    }

    /**
     * Adds a new account to the user collection of accounts. 
     *
     * @param account any
     */
    public final void addAccount(Account account) {
        if (account != null) {
            if (accounts == null) {
                accounts = new HashSet<Account>(1);
            }
            accounts.add(account);
            account.setOwner(this);
        }
    }

    /**
     * Filters accounts retrieving all where the fav flag is enabled.  
     *
     * @return a non null collection
     */
    public final SortedSet<Account> getFavoriteAccounts() {
        return filter(new Predicate() {
            @Override public boolean evaluate(Object object) {
                return ((Account) object).getFavorite();
            }
        });
    }

    public final SortedSet<Account> getBankAccounts() {
        return filter(new Predicate() {
            @Override public boolean evaluate(Object object) {
                return ((Account) object).isBankAccount();
            }
        });
    }

    public final SortedSet<Account> getInvestmentAccounts() {
        return filter(new Predicate() {
            @Override public boolean evaluate(Object object) {
                return ((Account) object).isInvestmentAccount();
            }
        });
    }

    public final SortedSet<Account> getBankAccounts(final Locale locale) {
        return filter(new Predicate() {
            @Override public boolean evaluate(Object object) {
                Account account = (Account) object;
                return account.isBankAccount() && account.getLocale().equals(locale);
            }
        });
    }

    protected final SortedSet<Account> filter(Predicate predicate) {
        SortedSet<Account> filtered = new TreeSet<Account>();
        if (!CollectionUtils.isEmpty(accounts)) {
            for (Account account : accounts) {
                if (predicate.evaluate(account)) {
                    filtered.add(account);
                }
            }
        }
        return Collections.unmodifiableSortedSet(filtered);
    }

    /**
     * Find user given the username (login).
     *
     * @param userName any
     * @return any
     */
    public static UserDetails findUserDetailsByUsername(String userName) {
        return entityManager().createQuery("select u from UserDetails u where username = :username", UserDetails.class).setParameter("username", userName).getSingleResult();
    }

    /**
     * Find the current logged in user.
     *
     * @return any
     */
    public static UserDetails findCurrentUser() {
        String userName = SecurityUtils.getRemoteUser();
        return hasText(userName) ? UserDetails.findUserDetailsByUsername(userName) : null;
    }

    public final void setSalt(String salt) {
        this.salt = salt;
    }

    @Override
    public String toString() {
        return getName();
    }

    /**
     * Users cannot be removed for now (NOP).
     */
    public void remove() {
        return;
    }
}
